use std::collections::BTreeMap;
use std::fmt::Debug;
use std::fs;
use std::path::{Path, PathBuf};

use config::ValueKind;
use image::DynamicImage;
use log::debug;
use ndarray::Array4;
use rand::distr::weighted::WeightedIndex;
use rand::distr::Distribution;

use crate::tiling::Direction;
use crate::Error;

pub(crate) mod tile_list;

#[cfg(test)]
mod tests {
    mod connectors;
    mod tile;
    mod tile_connection;
    mod tile_connection_array;
    mod tile_content;
    mod tile_type;
    mod tile_value_kind;
}

#[cfg_attr(test, derive(PartialEq))]
#[derive(clap::ValueEnum, Clone, Debug)]
pub(crate) enum TileType {
    Raster,
    Svg,
    Text,
}

impl TileType {
    fn determine_by_file_path(file_path: &PathBuf) -> Result<Self, Error> {
        // Determine the tile type.
        let tile_type = if Tile::new_raster_image_from_file(
            0,
            "".to_string(),
            &file_path,
            Connectors {
                north: 1,
                east: 1,
                south: 1,
                west: 1,
            },
            1.,
        )
        .is_ok()
        {
            TileType::Raster
        } else if Tile::new_text_from_file(
            0,
            "".to_string(),
            file_path,
            Connectors {
                north: 1,
                east: 1,
                south: 1,
                west: 1,
            },
            1.,
        )
        .is_ok()
        {
            TileType::Text
        } else {
            return Err(Error::ValueError(format!(
                "tile type could not be determined from the file {}",
                file_path.display(),
            )));
        };
        debug!("Determined tile type “{:?}”", tile_type);

        Ok(tile_type)
    }
}

#[cfg_attr(test, derive(Clone, Debug, PartialEq))]
pub(crate) enum TileContent {
    Empty,
    Text(Vec<String>),
    Raster(DynamicImage),
}

/// Represents the connectors of a tile.
///
/// Each field in this struct corresponds to a side of the tile and holds an
/// integer ID that represents a specific type of connector. These IDs are
/// mappings from user-defined connector names to integer identifiers used
/// internally for efficient processing.
///
/// The value `0` is reserved and represents the special wildcard connector
/// which is used to determine fitting tiles in cases where any connector is
/// possible. Consequently, IDs for concrete connectors begin at `1`. The value
/// `1` represents the blank connector. Neighboring tiles with the blank
/// connector facing each other expresses that these tiles are not connected.
/// This can be used to ensure that there are no disconnected areas inside the
/// grid.
#[cfg_attr(test, derive(Debug))]
#[derive(Clone, Eq, Hash, PartialEq)]
pub(crate) struct Connectors {
    pub(crate) north: usize,
    pub(crate) east: usize,
    pub(crate) south: usize,
    pub(crate) west: usize,
}

impl Connectors {
    /// Return the connector at the specified side.
    pub(crate) fn get_connector(&self, side: Direction) -> usize {
        match side {
            Direction::North => self.north,
            Direction::East => self.east,
            Direction::South => self.south,
            Direction::West => self.west,
        }
    }

    /// Set the connector at the specified side to a new value.
    pub(crate) fn set_connector(&mut self, side: Direction, value: usize) {
        match side {
            Direction::North => self.north = value,
            Direction::East => self.east = value,
            Direction::South => self.south = value,
            Direction::West => self.west = value,
        }
    }
}

#[cfg_attr(test, derive(Clone, Debug, PartialEq))]
pub(crate) struct Tile {
    pub(crate) id: usize,
    pub(crate) name: String,
    pub(crate) content: TileContent,
    // The connectors given by the user as strings are mapped to integer IDs, which are used here.
    pub(crate) connectors: Connectors,
    pub(crate) weight: f64,
}

impl Tile {
    pub(crate) fn new_text(
        id: usize,
        name: String,
        content: Vec<String>,
        connectors: Connectors,
        weight: f64,
    ) -> Result<Self, Error> {
        // Check if any line in content has a length differing from the others.
        // Taken from https://mastodon.social/@pingveno/102227428277791031
        if content
            .first()
            .map(|first| {
                content
                    .iter()
                    .any(|line| line.chars().count() != first.chars().count())
            })
            .unwrap_or(false)
        {
            Err(Error::ValueError(format!(
                "line lengths of the tile with the name “{name}” differ among each \
                 other:\n{content:#?}"
            )))
        } else {
            Ok(Self {
                id,
                name,
                content: TileContent::Text(content),
                connectors,
                weight,
            })
        }
    }

    fn new_text_from_file<P: AsRef<Path> + Debug + Clone>(
        id: usize,
        name: String,
        file_path: P,
        connectors: Connectors,
        weight: f64,
    ) -> Result<Self, Error> {
        match fs::read_to_string(file_path.clone()) {
            Ok(content) => Ok(Self::new_text(
                id,
                name,
                content.lines().map(|line| line.to_string()).collect(),
                connectors,
                weight,
            )?),
            Err(io_error) => Err(Error::IoError(format!(
                "the tile file located at {file_path:?} could not be opened: {}",
                io_error,
            ))),
        }
    }

    fn new_raster_image(
        id: usize,
        name: String,
        content: DynamicImage,
        connectors: Connectors,
        weight: f64,
    ) -> Self {
        Self {
            id,
            name,
            content: TileContent::Raster(content),
            connectors,
            weight,
        }
    }

    fn new_raster_image_from_file<P: AsRef<Path> + Debug + Clone>(
        id: usize,
        name: String,
        file_path: P,
        connectors: Connectors,
        weight: f64,
    ) -> Result<Self, Error> {
        match image::open(file_path.clone()) {
            Ok(file) => Ok(Self {
                id,
                name,
                content: TileContent::Raster(file),
                connectors,
                weight,
            }),
            Err(error) => Err(Error::IoError(format!(
                "the tile file located at {file_path:?} could not be loaded: {}",
                error
            ))),
        }
    }

    fn determine_constructor_by_tile_type<P: AsRef<Path> + Debug + Clone>(
        tile_type: &TileType,
    ) -> fn(usize, String, P, Connectors, f64) -> Result<Tile, Error> {
        match tile_type {
            TileType::Raster => Tile::new_raster_image_from_file,
            TileType::Svg => {
                unimplemented!()
            },
            TileType::Text => Tile::new_text_from_file,
        }
    }
}

pub trait RngCoreExt: rand::RngCore + Sized {}
impl<T: rand::RngCore + Sized> RngCoreExt for T {}

#[cfg_attr(test, derive(Debug, PartialEq))]
#[derive(Default)]
pub(crate) struct TileConnection {
    pub(crate) tiles: Vec<usize>,
    pub(crate) weights: Vec<f64>,
}

impl TileConnection {
    pub(crate) fn select_tile_by_priority(&self) -> usize {
        // Taken from https://stackoverflow.com/a/57963733
        let (max_idx, _) = self.weights.iter().enumerate().fold(
            (0, self.weights[0]),
            |(idx_max, val_max), (idx, val)| {
                if &val_max > val {
                    (idx_max, val_max)
                } else {
                    (idx, *val)
                }
            },
        );

        self.tiles[max_idx]
    }

    pub(crate) fn select_tile_randomly<R: RngCoreExt>(&self, rng: &mut R) -> Result<usize, Error> {
        let distribution = WeightedIndex::new(&self.weights).map_err(|err| {
            Error::ValueError(format!("selecting a tile randomly failed: {}", err))
        })?;
        Ok(self.tiles[distribution.sample(rng)])
    }
}

pub(crate) trait ArrayInitializable {
    fn new(tiles: &Vec<Tile>) -> Self;
}

/// An array with four dimensions, one for each kind of connector (north, east,
/// south, west). It maps all possible `Connector` variants to a
/// `TileConnection` instance that holds all tiles which satisfy the particular
/// `Connector` variant. It is used to determine the proper `TileConnection`
/// instance, which is used in `SlotGrid::determine_tile_connection()`. In each
/// dimension of `TileConnectionArray`, index `0` stands for the wildcard
/// connector, therefore every tile is added to index `[0, 0, 0, 0]`.
pub(crate) type TileConnectionArray = Array4<TileConnection>;

impl ArrayInitializable for TileConnectionArray {
    fn new(tiles: &Vec<Tile>) -> Self {
        // horizontal_connector_count and vertical_connector_count are both increased by
        // one to account for the wildcard connector with the ID 0.
        let horizontal_connector_count = tiles
            .iter()
            .flat_map(|tile| [tile.connectors.east, tile.connectors.west])
            .max()
            .unwrap_or(0)
            + 1;
        let vertical_connector_count = tiles
            .iter()
            .flat_map(|tile| [tile.connectors.north, tile.connectors.south])
            .max()
            .unwrap_or(0)
            + 1;

        // tile_connection_array has four dimensions, one for each kind of connector
        // (north, east, south, west). In each dimension, index 0 stands for the
        // wildcard connector, which stands for any concrete connector, index 1
        // stands for the blank connector.
        let mut tile_connection_array = Self::default((
            vertical_connector_count,
            horizontal_connector_count,
            vertical_connector_count,
            horizontal_connector_count,
        ));

        for (index, tile) in tiles.iter().enumerate() {
            // Add each tile to every TileConnection where its connectors match.
            // The wildcard connector (index 0) matches every connector, therefore every
            // tile is added to tile_connection_array[[0, 0, 0, 0]].
            for connector_indices in itertools::iproduct!(
                [0, tile.connectors.north],
                [0, tile.connectors.east],
                [0, tile.connectors.south],
                [0, tile.connectors.west]
            ) {
                tile_connection_array[connector_indices].tiles.push(index);
                tile_connection_array[connector_indices]
                    .weights
                    .push(tile.weight);
            }
        }

        tile_connection_array
    }
}

trait Unpackable {
    fn unpack_numeric_to_f64(&self) -> Result<f64, Error>;
    fn unpack_string(&self) -> Result<String, Error>;
    fn unpack_string_or_nil(&self) -> Result<Option<String>, Error>;
    fn unpack_table(&self) -> Result<BTreeMap<String, &ValueKind>, Error>;
    fn unpack_array(&self) -> Result<Vec<&ValueKind>, Error>;
    fn explain_unexpected_kind<T>(&self, expected: &str) -> Result<T, Error>;
}

type TileValueKind = ValueKind;

impl Unpackable for TileValueKind {
    fn unpack_numeric_to_f64(&self) -> Result<f64, Error> {
        match self {
            ValueKind::Float(float) => Ok(*float),
            ValueKind::I64(integer) => Ok(*integer as f64),
            ValueKind::I128(integer) => Ok(*integer as f64),
            ValueKind::U64(unsigned) => Ok(*unsigned as f64),
            ValueKind::U128(unsigned) => Ok(*unsigned as f64),
            value_kind => {
                value_kind.explain_unexpected_kind("a numeric (Float, I64, I128, U64, U128)")
            },
        }
    }

    fn unpack_string(&self) -> Result<String, Error> {
        match self {
            ValueKind::String(string) => Ok(string.clone()),
            value_kind => value_kind.explain_unexpected_kind("a String"),
        }
    }

    fn unpack_string_or_nil(&self) -> Result<Option<String>, Error> {
        match self {
            ValueKind::String(string) => Ok(Some(string.clone())),
            ValueKind::Nil => Ok(None),
            value_kind => value_kind.explain_unexpected_kind("a String"),
        }
    }

    fn unpack_table(&self) -> Result<BTreeMap<String, &ValueKind>, Error> {
        match self {
            ValueKind::Table(table) => Ok(table
                .iter()
                .map(|(key, value)| (key.clone(), &value.kind))
                .collect()),
            value_kind => value_kind.explain_unexpected_kind("a Table (Map)"),
        }
    }

    fn unpack_array(&self) -> Result<Vec<&ValueKind>, Error> {
        match self {
            ValueKind::Array(array) => Ok(array.iter().map(|element| &element.kind).collect()),
            value_kind => value_kind.explain_unexpected_kind("an Array"),
        }
    }

    fn explain_unexpected_kind<T>(&self, expected: &str) -> Result<T, Error> {
        let one_line_display = format!(" {self:?}");
        let display = if one_line_display.len() > 32 {
            format!("\n{self:#?}")
        } else {
            one_line_display
        };
        Err(Error::ValueError(format!(
            "expected {expected}, got{display}"
        )))
    }
}
